package com.cfcoding.validation;

import java.io.*;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

/**
 * This class validates an e-mail address by determining the mail exchange servers
 * for the domain, querying each of the servers' SMTP protocol to validate that
 * the e-mail address exists on the server.
 * <p>
 * Example usage:<br>
 * <pre>new EmailValidator("yourname@domain.com").isAddressValid();</pre>
 * @author Tristan Lee (tristanlee85@gmail.com)
 */
public class EmailValidator {

    private String email = null;
    private String domain = null;
    private Socket smtpSoc = null;
    private DataOutputStream os = null;
    private DataInputStream in = null;

    /**
     * Constructor
     * @param email E-mail address to validate
     */
    public EmailValidator(String email) {
        this.email = email;
        this.domain = email.substring(email.indexOf("@") + 1);
    }

    /**
     * Gets the e-mail address being validated
     * @return the e-mail address
     */
    public String getEmail() {
        return this.email;
    }

    /**
     * Gets the domain of the mail exchangers
     * @return the e-mail domain
     */
    public String getDomain() {
        return this.domain;
    }

    /**
     * Gets the available MX servers for this domain
     *
     * @return the available MX servers
     * @throws NamingException
     */
    public ArrayList getMailServers() throws NamingException {
        // Perform a DNS lookup for MX records in the domain
        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put("java.naming.factory.initial",
                "com.sun.jndi.dns.DnsContextFactory");
        DirContext ictx = new InitialDirContext(env);
        Attributes attrs = ictx.getAttributes(this.domain, new String[]{"MX"});
        Attribute attr = attrs.get("MX");

        // If there is no MX record found, try the domain name
        if ((attr == null) || (attr.size() == 0)) {
            attrs = ictx.getAttributes(this.domain, new String[]{"A"});
            attr = attrs.get("A");
            if (attr == null) {
                throw new NamingException("No match for name '" + this.domain + "'");
            }
        }

        // Create a list of the available servers
        ArrayList res = new ArrayList();
        NamingEnumeration en = attr.getAll();

        while (en.hasMore()) {
            String x = (String) en.next();
            String f[] = x.split(" ");
            int idx = f.length == 2 ? 1 : 0;
            if (f[idx].endsWith(".")) {
                f[idx] = f[idx].substring(0, (f[idx].length() - 1));
            }
            res.add(f[idx]);
        }
        return res;
    }

    /**
     * Validates the e-mail address against all of the available mail exchangers
     * @return true if the address is valid
     */
    public boolean isAddressValid() {
        // Find the separator for the domain name
        int pos = this.email.indexOf('@');

        // If the address does not contain an '@', it's not valid
        if (pos == -1) {
            return false;
        }

        // Get the mail exchangers
        ArrayList mxList;
        try {
            mxList = getMailServers();
        } catch (NamingException ex) {
            return false;
        }

        // If there are no MX servers, the e-mail address doesn't exist
        if (mxList.isEmpty()) {
            return false;
        }

        // Loops through the exchangers and validate the e-mail address; it's
        // possible for a server to reject the mail, but another one to accept
        // so we must check all
        for (int mx = 0; mx < mxList.size(); mx++) {
            boolean valid = false;
            try {
                int res;
                smtpSoc = new Socket((String) mxList.get(mx), 25);
                BufferedReader rdr = new BufferedReader(new InputStreamReader(smtpSoc.getInputStream()));
                BufferedWriter wtr = new BufferedWriter(new OutputStreamWriter(smtpSoc.getOutputStream()));
                res = read(rdr);
                if (res != 220) {
                    throw new Exception("Invalid header");
                }
                
                send(wtr, "EHLO validemail.com");
                res = read(rdr);
                if (res != 250) {
                    throw new Exception("Not ESMTP");
                }
                
                // Validate the sender address  
                send(wtr, "MAIL FROM: <is@validemail.com>");
                res = read(rdr);
                if (res != 250) {
                    throw new Exception("Sender rejected");
                }
                
                send(wtr, "RCPT TO: <" + email + ">");
                res = read(rdr);
                
                // Close the connection
                send(wtr, "RSET");
                read(rdr);
                send(wtr, "QUIT");
                read(rdr);
                if (res != 250) {
                    throw new Exception("Address is not valid!");
                }

                valid = true;
                rdr.close();
                wtr.close();
                smtpSoc.close();
            } catch (Exception ex) {
            } finally {
                if (valid) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Sends a command to the socket
     *
     * @param bw
     * @param command
     * @throws IOException
     */
    private void send(BufferedWriter bw, String command) throws IOException {
        bw.write(command + "\r\n");
        bw.flush();
    }

    /**
     * Reads socket response
     *
     * @param br
     * @return
     * @throws IOException
     */
    private int read(BufferedReader br) throws IOException {
        String line;
        int res = 0;

        while ((line = br.readLine()) != null) {
            String pfx = line.substring(0, 3);
            try {
                res = Integer.parseInt(pfx);
            } catch (Exception ex) {
                res = -1;
            }
            if (line.charAt(3) != '-') {
                break;
            }
        }

        return res;
    }
}
